package org.zoomdy.gdbx;

import java.nio.ByteBuffer;

/*
 * Node 负责管理连接和数据收发，Packet 负责数据解析。
 */
public class Packet
{
    public enum ProtocolType
    {
        GDB,
        LINE,
    }
    
    public enum PacketType
    {
        UNDEF,
        HEX,
        BIN,
        ACK,
        NACK,
        CTRLC,
        LINE,
    }
    
    
    static final int BUFFER_CAPACITY   = 1 * 1024 * 1024;
    
    ProtocolType protocolType;
    PacketType packetType;
    boolean valid;
    
    int channels;
    ByteBuffer buffer;
    String str;
    String strRaw;
    
    String vFlashWrite = "#vFlashWrite";
    byte[] vFlashWriteArray = vFlashWrite.getBytes();
    
    public Packet()
    {
        this.packetType = PacketType.UNDEF;
        this.valid = false;
        this.str = null;
        this.strRaw = null;
    }
    
    private ByteBuffer fetchGdb(ByteBuffer buffer)
    {
        assert buffer.position() == 0;
        assert buffer.hasRemaining();
            
        byte b0 = buffer.get(0);
        if(b0 == '+' || b0 == '-' || b0 == 0x03)
        {
            this.buffer = ByteBuffer.allocate(1);
            this.buffer.put(buffer.get());
            this.buffer.flip();
            buffer.compact();
            buffer.flip();
            
            if(b0 == '+')
            {
                this.packetType = PacketType.ACK;
            }
            else if(b0 == '-')
            {
                this.packetType = PacketType.NACK;
            }
            else
            {
                this.packetType = PacketType.CTRLC;
            }
        }
        else if(b0 == '$')
        {
            int len = buffer.limit();
            byte b_3 = buffer.get(len - 3);
            if(b_3 == '#' && len >= 4)
            {
                this.buffer = buffer;
                buffer = ByteBuffer.allocate(Packet.BUFFER_CAPACITY);
                buffer.flip();
                
                byte b1 = this.buffer.get(1);
                if(b1 == 'X') // X is binary data packet
                {
                    this.packetType = PacketType.BIN;
                }
                else if(b1 == 'v') // vFlashWrite is binary data packet
                {
                    for(int i = 2; i < this.vFlashWriteArray.length; i++)
                    {
                        if(this.buffer.get(i) != this.vFlashWriteArray[i])
                        {
                            this.packetType = PacketType.HEX;
                            break;
                        }
                    }
                    
                    if(this.packetType == PacketType.UNDEF)
                    {
                        this.packetType = PacketType.BIN;
                    }
                }
                else
                {
                    this.packetType = PacketType.HEX;
                }
            }
        }
        
        if(this.packetType != PacketType.UNDEF)
        {
            this.valid = true;
        }
        
        return buffer;
    }
    
    private ByteBuffer fetchLine(ByteBuffer buffer)
    {
        assert buffer.position() == 0;
        assert buffer.hasRemaining();
        
        int len = buffer.limit();
        byte b_1 = buffer.get(len - 1);
        
        if(b_1 == '\r' || b_1 == '\n')
        {
            this.buffer = buffer;
            buffer = ByteBuffer.allocate(Packet.BUFFER_CAPACITY);
            buffer.flip();
            
            this.packetType = PacketType.LINE;
        }
        
        if(this.packetType != PacketType.UNDEF)
        {
            this.valid = true;
        }
        
        return buffer;
    }
    
    public ByteBuffer fetch(ByteBuffer buffer)
    {
        return this.fetchGdb(buffer);
    }
    
    public ByteBuffer fetch(ByteBuffer buffer, ProtocolType protocol)
    {
        
        if(protocol == ProtocolType.GDB)
        {
            return this.fetchGdb(buffer);
        }
        else
        {
            return this.fetchLine(buffer);
        }
    }
    
    public boolean isValid()
    {
        return this.valid;
    }
    
    public int getChannels()
    {
        return channels;
    }

    public void setChannels(int channels)
    {
        this.channels = channels;
    }

    public ProtocolType getProtocolType()
    {
        return protocolType;
    }

    public PacketType getPacketType()
    {
        return this.packetType;
    }
    
    public ByteBuffer getBuffer()
    {
        return this.buffer;
    }
    
    public String getTypeName(PacketType packetType)
    {
        if(packetType == PacketType.HEX)
        {
            return "GDB packet (normal)";
        }
        else if(packetType == PacketType.BIN)
        {
            return "GDB packet (binary)";
        }
        else if(packetType == PacketType.ACK)
        {
            return "GDB packet (ack)";
        }
        else if(packetType == PacketType.NACK)
        {
            return "GDB packet (nack)";
        }
        else if(packetType == PacketType.CTRLC)
        {
            return "GDB packet (Ctrl+C)";
        }
        else if(packetType == PacketType.LINE)
        {
            return "Normal text (console output)";
        }
        else
        {
            return "Unknown packet (undef)";
        }
    }
    
    /**
     * 检查该数据包是否将目标机置于运行状态。
     * @return
     */
    public boolean isRunPacket()
    {
        if(!this.isValid())
        {
            return false;
        }
        
        if(this.packetType != PacketType.HEX)
        {
            return false;
        }
        
        assert this.buffer != null;
        assert this.buffer.limit() >= 4;
        assert this.buffer.get(0) == '$';
        assert this.buffer.get(this.buffer.limit() - 3) == '#';
        
        byte b1 = this.buffer.get(1);
        if(b1 == 'c' || b1 == 'C' || b1 == 's' || b1 == 'S' || b1 == 'i' || b1 == 'I')
        {
            return true;
        }
        
        // vCont;
        if(b1 == 'v' && this.buffer.limit() > 6 &&
                this.buffer.get(2) == 'C' && 
                this.buffer.get(3) == 'o' &&
                this.buffer.get(4) == 'n' &&
                this.buffer.get(5) == 't' &&
                this.buffer.get(6) == ';')
        {
            return true;
        }
        
        return false;
    }
    
    /**
     * 检查该数据包是否是目标机停止时产生的。
     * @return
     */
    public boolean isStopPacket()
    {
        if(!this.isValid())
        {
            return false;
        }
        
        if(this.packetType != PacketType.HEX)
        {
            return false;
        }
        
        assert this.buffer != null;
        assert this.buffer.limit() >= 4;
        assert this.buffer.get(0) == '$';
        assert this.buffer.get(this.buffer.limit() - 3) == '#';
        
        byte b1 = this.buffer.get(1);
        if(b1 == 'S' || b1 == 'T' || b1 == 'W' || b1 == 'X')
        {
            return true;
        }
        
        return false;
    }
    
    /**
     * 将数据转换成以大写字母O为首，输出内容Hex编码的GDB调试输出包。
     * @return 如果转换成功返回GDB调试输出包，否则返回null。
     */
    public Packet toGdbOutputPacket()
    {
        if(this.packetType != PacketType.LINE)
        {
            return null;
        }
        
        int len = this.buffer.limit();
        Packet packet = new Packet();
        packet.buffer = ByteBuffer.allocate(2 + len * 2 + 3);
        packet.buffer.put((byte) '$'); // Packet start token
        packet.buffer.put((byte) 'O'); // Console Output packet type
        
        while(this.buffer.hasRemaining())
        {
            byte b = this.buffer.get();
            
            packet.buffer.put(packet.toHexHi(b));
            packet.buffer.put(packet.toHexLo(b));
        }
        
        packet.buffer.put((byte) '#'); // Packet end token
        packet.checksum(packet.buffer.capacity()); // Check sum
        packet.buffer.flip();
        
        packet.channels = this.channels;
        packet.packetType = PacketType.HEX;
        packet.protocolType = ProtocolType.GDB;
        packet.valid = true;
        
        this.valid = false;
        
        return packet;
    }
    
    public void checksum(int packetTotalLength)
    {
        assert this.buffer.get(0) == '$';
        assert this.buffer.get(packetTotalLength - 3) == '#';
        assert packetTotalLength >= 4;
        int sum = 0;
        
        for(int i = 1; i < packetTotalLength - 3; i++)
        {
            sum += this.buffer.get(i);
        }
        sum &= 0xff;
        
        this.buffer.put(this.toHexHi((byte)sum));
        this.buffer.put(this.toHexLo((byte)sum));
    }
    /*
     * 将二进制字节数据的低4位转换成1位HEX字符
     */
    public byte toHexLo(byte b)
    {
        int b0 = b & 0xf;
        byte h0;
        if(b0 < 10)
        {
            h0 = (byte)('0' + b0);
        }
        else
        {
            h0 = (byte)('a' + b0 - 10);
        }
        
        
        return h0;
    }
    
    /*
     * 将二进制字节数据的高4位转换成1位HEX字符
     */
    public byte toHexHi(byte b)
    {
        int b1 = (b >> 4) & 0xf;
        byte h1;
        
        if(b1 < 10)
        {
            h1 = (byte)('0' + b1);
        }
        else
        {
            h1 = (byte)('a' + b1 - 10);
        }
        
        return h1;
    }
    
    /*
     * 将2字节的HEX字符数据转换为二进制字节
     */
    public byte toByte(byte b1, byte b0)
    {
        assert (b1 >= '0' && b1 <= '9') || (b1 >= 'A' && b1 <= 'F');
        assert (b0 >= '0' && b0 <= '9') || (b0 >= 'A' && b0 <= 'F');
        
        if(b1 >= '0' && b1 <= '9')
        {
            b1 = (byte) (b1 - '0');
        }
        else
        {
            b1 = (byte) (b1 - 'A' + 10);
        }
        
        if(b0 >= '0' && b0 <= '9')
        {
            b0 = (byte) (b0 - '0');
        }
        else
        {
            b0 = (byte) (b0 - 'A' + 10);
        }
        
        byte b = (byte) ((b1 << 4) | b0);
        
        return b;
    }
    
    public boolean isPrint()
    {
        byte b;
        for(int i = 0; i < this.buffer.limit(); i++)
        {
            b = this.buffer.get(i);
            if(b > 0x7e || (b < 0x20 && b != 0x0a && b != 0x0d))
            {
                return false;
            }
        }
        
        return true;
    }
    
    public boolean startsWith(String start)
    {
        return this.getString().startsWith(start);
    }
    public boolean equals(String eq)
    {
        return this.getString().equals(eq);
    }
    
    public String getString()
    {
        if(this.str == null)
        {
            if(this.packetType == Packet.PacketType.HEX)
            {
                assert this.buffer.hasArray() == true;
                assert this.buffer.position() == 0;
                assert this.buffer.limit() >= 4;
                
                this.str = new String(this.buffer.array(), 1, this.buffer.limit() - 4);
            }
            else if(this.packetType == Packet.PacketType.ACK || this.packetType == Packet.PacketType.NACK)
            {
                this.str = new String(this.buffer.array(), 0, 1);
            }
            else if(this.packetType == Packet.PacketType.CTRLC)
            {
                this.str = "***Ctrl+C***";
            }
            else if(this.packetType == Packet.PacketType.LINE)
            {
                this.str = new String(this.buffer.array(), 0, this.buffer.limit());
            }
            else if(this.packetType == Packet.PacketType.BIN)
            {
                assert this.buffer.hasArray() == true;
                assert this.buffer.position() == 0;
                assert this.buffer.limit() >= 4;
                
                int i;
                byte b;
                for(i = 2; i < this.buffer.limit(); i++)
                {
                    b = this.buffer.get(i);
                    if(b < 0x20 || b > 0x7E)
                    {
                        break;
                    }
                }
                
                this.str = new String(this.buffer.array(), 1, i - 1);
                this.str += "***BinaryData***";
            }
            else
            {
                this.str = "***UndefinedPacket***";
            }
        }
        
        return this.str;
    }
    
    public String getRawString()
    {
        if(this.strRaw == null)
        {
            if(this.packetType == Packet.PacketType.HEX)
            {
                assert this.buffer.hasArray() == true;
                assert this.buffer.position() == 0;
                assert this.buffer.limit() >= 4;
                
                this.strRaw = new String(this.buffer.array(), 0, this.buffer.limit());
            }
            else if(this.packetType == Packet.PacketType.ACK || this.packetType == Packet.PacketType.NACK)
            {
                this.strRaw = new String(this.buffer.array(), 0, 1);
            }
            else if(this.packetType == Packet.PacketType.CTRLC)
            {
                this.strRaw = "***Ctrl+C***";
            }
            else if(this.packetType == Packet.PacketType.LINE)
            {
                this.strRaw = new String(this.buffer.array(), 0, this.buffer.limit());
            }
            else if(this.packetType == Packet.PacketType.BIN)
            {
                assert this.buffer.hasArray() == true;
                assert this.buffer.position() == 0;
                assert this.buffer.limit() >= 4;
                
                int i;
                byte b;
                for(i = 2; i < this.buffer.limit(); i++)
                {
                    b = this.buffer.get(i);
                    if(b < 0x20 || b > 0x7E)
                    {
                        break;
                    }
                }
                
                this.strRaw = new String(this.buffer.array(), 0, i);
                this.strRaw += "***BinaryData***";
            }
            else
            {
                this.strRaw = "***UndefinedPacket***";
            }
        }
        
        return this.strRaw;
    }
    
    public static Packet make(String cmd)
    {
        assert cmd != null;
        
        Packet packet = new Packet();
        packet.buffer = ByteBuffer.allocate(1 + cmd.length() + 3);
        packet.buffer.put((byte) '$'); // Packet start token
        
        for(byte b : cmd.getBytes()) // Packet Content
        {
            packet.buffer.put(b);
        }
        
        packet.buffer.put((byte) '#'); // Packet end token
        packet.checksum(packet.buffer.capacity()); // Check sum
        packet.buffer.flip();
        
        packet.channels = 0;
        packet.packetType = PacketType.HEX;
        packet.protocolType = ProtocolType.GDB;
        packet.str = cmd;
        packet.valid = true;
        
        return packet;
    }
    
    public static Packet makeHex(String ascii)
    {
        assert ascii != null;
        
        Packet packet = new Packet();
        packet.buffer = ByteBuffer.allocate(1 + ascii.length() * 2 + 3);
        packet.buffer.put((byte) '$'); // Packet start token
        
        for(byte b : ascii.getBytes()) // Packet Content
        {
            packet.buffer.put(packet.toHexHi(b));
            packet.buffer.put(packet.toHexLo(b));
        }
        
        packet.buffer.put((byte) '#'); // Packet end token
        packet.checksum(packet.buffer.capacity()); // Check sum
        packet.buffer.flip();
        
        packet.channels = 0;
        packet.packetType = PacketType.HEX;
        packet.protocolType = ProtocolType.GDB;
        packet.str = null;
        packet.valid = true;
        
        return packet;
    }
    
    public static Packet makeReadMemory(long addr, long length)
    {
        assert addr >= 0L && addr <= 0xffffffffL;
        assert length > 0L && length <= 0x10000L;
        assert addr + length <= 0x100000000L;
        
        String cmd;
        cmd = String.format("m%x,%x", addr, length);
        
        return Packet.make(cmd);
    }
    
    public static Packet makeSpecial(PacketType type)
    {
        assert type == PacketType.ACK || type == PacketType.NACK || type == PacketType.CTRLC;
        
        Packet packet = new Packet();
        
        packet.buffer = ByteBuffer.allocate(1);
        if(type == PacketType.ACK)
        {
            packet.buffer.put((byte) '+');
        }
        else if(type == PacketType.NACK)
        {
            packet.buffer.put((byte) '-');
        }
        else if(type == PacketType.CTRLC)
        {
            packet.buffer.put((byte)0x03);
        }
        else
        {
            
        }
        
        packet.buffer.flip();
        
        packet.channels = 0;
        packet.packetType = type;
        packet.protocolType = ProtocolType.GDB;
        packet.valid = true;
        
        return packet;
    }
}
